<?php

/**
 * SkinDetection class
 *
 * @copyright	Copyright (c) Kevin Kettinger <kkettinger@gmail.com>
 */
class SkinDetection {

	/**
	 * threshold for hue (H)
	 */
	private $threshold_h_min = 0;
	private $threshold_h_max = 0;

	/**
	 * threshold for saturation (S)
	 */
	private $threshold_s_min = 0;
	private $threshold_s_max = 0;

	/**
	 * resize width and height
	 */
	private $resize_width = null;
	private $resize_height = null;

	/**
	 * max width and height for processing
	 */
	private $max_width = 4000;
	private $max_height = 4000;

	/**
	 * skin colors in rgb-format
	 */
	private $skin_colors = array();

	/**
	 * input file
	 */
	private $input_file = null;

	/**
	 * output file
	 */
	private $output_file = null;

	/**
	 * enable imagemagick for better format support
	 */
	private $imagemagick_enable = false;


	/**
	 * Public constructor
	 *
	 * When $output_file is set, an binary image
	 * with detected skin will be created.
	 */
	public function __construct($input_file, $output_file = null) {
		$this->input_file = $input_file;

		if ($output_file !== null) {
			$this->output_file = $output_file;
		}
	}

	/**
	 * Sets the threshold for hue
	 */
	public function setThresholdH($min, $max) {
		$this->threshold_h_min = $min;
		$this->threshold_h_max = $max;
	}

	/**
	 * Sets the threshold for saturation
	 */
	public function setThresholdS($min, $max) {
		$this->threshold_s_min = $min;
		$this->threshold_s_max = $max;
	}

	/**
	 * loads a file with skin-colors
	 *
	 * each line should have the following format:
	 *  -> red,green,blue\n <-
	 */
	public function loadSkinColors($file) {
		if (!file_exists($file)) {
			throw new Exception("skin color file not found: ".$file);
		}

		// init colors array
		$colors = array();

		// open file
		$handle = fopen ($file, "r");
		while (!feof($handle)) {
			$buffer = trim(fgets($handle, 4096));

			// skip comments
			if (substr($buffer, 0, 1) == "#") {
				continue;
			}

			$explode = explode(",", $buffer);

			// add to array
			$colors[] = array((int)$explode[0], (int)$explode[1], (int)$explode[2]);
		}
		fclose ($handle);

		$this->skin_colors = $colors;
	}

	/**
	 * Enable resize of the image for faster processing
	 *
	 * When height is not given, the aspect ratio
	 * is calculated for height in $this->process()
	 */
	public function enableResize($width, $height = null) {
		$this->resize_width = $width;

		if ($height !== null) {
			$this->resize_height = $height;
		}
	}

	/**
	 * Enable ImageMagick
	 */
	public function enableImageMagick($enable) {
		$this->imagemagick_enable = (bool)$enable;
	}

	/**
	 * Main method for comparing a color against
	 * the skin-color database
	 */
	private function isSkin($r, $g, $b) {

		// transform to hsv-colorspace
		$hsv = $this->transformRGB2HSV($r, $g, $b);

		// go trough each entry of the skin-colors
		foreach ($this->skin_colors as $color){

			// transform each entry to the hsv-colorspace
			// @todo color-database in hsv
			$hsv_skin = $this->transformRGB2HSV($color[0], $color[1], $color[2]);

			// range for hue
			$range_h_min = ($hsv_skin["h"] - $this->threshold_h_min < 0) ? 0 : $hsv_skin["h"] - $this->threshold_h_min;
			$range_h_max = ($hsv_skin["h"] + $this->threshold_h_max > 1) ? 1 : $hsv_skin["h"] + $this->threshold_h_max;

			// range for saturation
			$range_s_min = ($hsv_skin["s"] - $this->threshold_s_min < 0) ? 0 : $hsv_skin["s"] - $this->threshold_s_min;
			$range_s_max = ($hsv_skin["s"] + $this->threshold_s_max > 1) ? 1 : $hsv_skin["s"] + $this->threshold_s_max;

			// test color
			if (	$hsv["h"] >= $range_h_min and $hsv["h"] <= $range_h_max
				and $hsv["s"] >= $range_s_min and $hsv["s"] <= $range_s_max
			) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get skin-colors
	 */
	public function getSkinColors() {
		return $this->skin_colors;
	}

	/**
	 * Process image
	 */
	public function process() {

		// input file exists?
		if (!file_exists($this->input_file)) {
			throw new Exception("file not found: ".$this->input_file);
		}

		// gd-lib module enabled?
		if (!function_exists("imagecreatefromjpeg")) {
			throw new Exception("gd-lib not enabled");
		}

		// process image with imagemagick
		if ($this->imagemagick_enable) {

			// save image in tmp file
			$image_tmp = "/tmp/".md5($this->input_file.time()).".jpg";

			// resize
			$resize_arg = null;

			if ($this->resize_width > 0) {

				// aspect ratio
				if ($this->resize_height === null) {
					$resize_arg = $this->resize_width."x";
				} else {
					$resize_arg = $this->resize_width."x".$this->resize_height;
				}
			}

			// convert input-image to jpeg
			if ($resize_arg !== null) {
				$exit_code = (int)shell_exec("convert -thumbnail ".$resize_arg." ".escapeshellarg($this->input_file."[0]")." ".escapeshellarg($image_tmp)." > /dev/null 2>&1; echo $?");
			} else {
				$exit_code = (int)shell_exec("convert ".escapeshellarg($this->input_file."[0]")." ".escapeshellarg($image_tmp)." > /dev/null 2>&1; echo $?");
			}

			// when convert returns not 0 as exit-code
			// something went wrong
			if ($exit_code != 0) {
				throw new Exception("convert failed: ".$exit_code);
			}

			// set tmp-image to input-file
			$this->input_file = $image_tmp;

			// create image-resource
			$image = imagecreatefromjpeg($this->input_file);

			// delete tmp-image
			shell_exec("rm ".escapeshellarg($image_tmp));

			// get resolution
			$width = ImageSX($image);
			$height = ImageSY($image);

		// process image with gd
		} else {

			// get extension
			$extension = strtolower(pathinfo($this->input_file, PATHINFO_EXTENSION));

			switch($extension){
				case "jpg":
				case "jpeg":
					$image = @imagecreatefromjpeg($this->input_file);
					break;

				case "png":
					$image = @imagecreatefrompng($this->input_file);
					break;

				case "gif":
					$image = @imagecreatefromgif($this->input_file);
					break;

				default:
					throw new Exception("image extension ".$extension." not supported.");
					break;
			} // switch

			if (!$image) {
				throw new Exception("cannot process image ".$this->input_file);
			}

			// get resolution
			$width = ImageSX($image);
			$height = ImageSY($image);

			// resize image
			if ($this->resize_width > 0) {

				// calculate aspect ratio
				if ($this->resize_height === null) {
					$factor = $this->resize_width / $width;
					$this->resize_height = $factor * $height;
				}

				$image_resize = imagecreatetruecolor($this->resize_width, $this->resize_height);
				imagecopyresized($image_resize, $image, 0, 0, 0, 0, $this->resize_width, $this->resize_height, $width, $height);

				// overwrite resize
				$image = $image_resize;

				// destroy resize image resource
				imagedestroy($image_resize);

				// overwrite width/height with resize-values
				$width = $this->resize_width;
				$height = $this->resize_height;
			}
		}

		// init return array
		$return = array("pixels_all" => $width * $height, "pixels_skin" => array(), "percent" => 0);

		// skip processing when image is bigger than max_width/height
		if ($width >= $this->max_width or $height >= $this->max_height) {
			throw new Exception("too high image resolution (w:".$width.",h:".$height.")");
		}

		// output file
		if ($this->output_file !== null) {

			// get extension
			$extension = strtolower(pathinfo($this->output_file, PATHINFO_EXTENSION));

			switch($extension){
				case "jpg":
				case "jpeg":
					$image_output = @ImageCreateTruecolor($width, $height);
					break;

				case "png":
					$image_output = @ImageCreateTruecolor($width, $height);
					break;

				case "gif":
					$image_output = @ImageCreateTruecolor($width, $height);
					break;

				default:
					throw new Exception("image extension ".$extension." not supported.");
					break;
			} // switch

			if (!$image) {
				throw new Exception("cannot create image ".$this->output_file);
			}

			// color of pixel
			$color_pixel = imagecolorallocate($image_output, 255, 255, 255);
		}

		// go over each pixel of the image
		for ($y = 0; $y < $height; $y++) {
			for ($x = 0; $x < $width; $x++) {

				// get color from current pixel
				$color = imagecolorsforindex($image, imagecolorat($image, $x, $y));

				// is the current color of the pixel skin?
				if ($this->isSkin($color["red"], $color["green"], $color["blue"])) {

					// add pixel to array
					$return["pixels_skin"][] = array("x" => $x, "y" => $x, "color" => array($color["red"], $color["green"], $color["blue"]));

					// print pixel on output image
					if ($this->output_file !== null) {
						imagesetpixel($image_output, $x, $y, $color_pixel);
					}
				}
			}
		}

		// close output file
		if ($this->output_file !== null) {
			imagepng($image_output, $this->output_file);
		}

		// destroy image resource
		imagedestroy($image);

		// calculate percentage
		$return["percent"] = count($return["pixels_skin"]) / $return["pixels_all"];

		return $return;
	}

	/**
	 * Method for transforming the rgb-colorspace
	 * to the hsv-colorspace.
	 *
	 * @copyright Not me.
	 */
	private function transformRGB2HSV($R, $G, $B) {
		$HSL = array();

		$var_R = ($R / 255);
		$var_G = ($G / 255);
		$var_B = ($B / 255);

		$var_Min = min($var_R, $var_G, $var_B);
		$var_Max = max($var_R, $var_G, $var_B);
		$del_Max = $var_Max - $var_Min;

		$V = $var_Max;

		if ($del_Max == 0)
		{
			$H = 0;
			$S = 0;
		}
		else
		{
			$S = $del_Max / $var_Max;

			$del_R = ( ( ( $max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
			$del_G = ( ( ( $max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
			$del_B = ( ( ( $max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;

			if      ($var_R == $var_Max) $H = $del_B - $del_G;
			else if ($var_G == $var_Max) $H = ( 1 / 3 ) + $del_R - $del_B;
			else if ($var_B == $var_Max) $H = ( 2 / 3 ) + $del_G - $del_R;

			if (H<0) $H++;
			if (H>1) $H--;
		}

		$HSL['h'] = $H;
		$HSL['s'] = $S;
		$HSL['v'] = $V;

		return $HSL;
	}

	/**
	 * Method for transforming the hsv-colorspace
	 * to the rgb-colorspace.
	 *
	 * @copyright Not me.
	 */
	private function transformHSV2RGB($H, $S, $V) {
		$RGB = array();

		if($S == 0)
		{
			$R = $G = $B = $V * 255;
		}
		else
		{
			$var_H = $H * 6;
			$var_i = floor( $var_H );
			$var_1 = $V * ( 1 - $S );
			$var_2 = $V * ( 1 - $S * ( $var_H - $var_i ) );
			$var_3 = $V * ( 1 - $S * (1 - ( $var_H - $var_i ) ) );

			switch($var_i) {
				case 0: {
					$var_R = $V; 
					$var_G = $var_3; 
					$var_B = $var_1;
					break;
				}
				case 1: {
					$var_R = $var_2; 
					$var_G = $V; 
					$var_B = $var_1;
					break;
				}
				case 2: {
					$var_R = $var_1; 
					$var_G = $V; 
					$var_B = $var_3;
					break;
				}
				case 3: {
					$var_R = $var_1; 
					$var_G = $var_2; 
					$var_B = $V;
					break;
				}
				case 4: {
					$var_R = $var_3; 
					$var_G = $var_1; 
					$var_B = $V;
					break;
				}
				default: {
					$var_R = $V; 
					$var_G = $var_1; 
					$var_B = $var_2;
				}
			}

			$R = $var_R * 255;
			$G = $var_G * 255;
			$B = $var_B * 255;
		}

		$RGB['r'] = $R;
		$RGB['g'] = $G;
		$RGB['b'] = $B;

		return $RGB;
	}
}
